P01-2. 서비스 메시의 탄생과 Envoy xDS - 실습 검증

Part 01 (실습): L4 Keep-Alive 쏠림 재현부터 Istio 설치 · Envoy xDS 5단계 해부까지

실습 검증 목표

  1. 서비스 메시가 없는 기본 쿠버네티스 환경에서 HTTP Keep-Alive 요청이 한쪽 파드로만 100% 쏠리는 현상(L4 Connection Pinning) 을 직접 재현한다.
  2. Istio 1.31.3 컨트롤 플레인(istiod)을 설치하고, 네임스페이스에 라벨 하나(istio-injection=enabled)를 붙여 파드 옆에 Envoy 사이드카(2/2) 를 자동으로 끼워 넣는다.
  3. 사이드카가 붙은 뒤 똑같은 Keep-Alive 요청을 보냈을 때, Envoy(L7)가 요청을 한 장씩 뜯어 두 파드로 골고루 나눠 주는 것을 눈으로 확인한다.
  4. istioctl proxy-config 명령어로 Envoy 메모리 속의 xDS 5단계(LDS → RDS → CDS → EDS + SDS) 를 하나씩 직접 열어보고 읽는 법을 익힌다.

사전 환경 확인

  • 00. 사전 준비에서 설정한 Docker Desktop 내장 3노드 클러스터(docker-desktop)가 켜져 있어야 합니다.
  • Windows PowerShell 창을 열고 아래 명령어로 작업 폴더 이동과 클러스터 상태를 먼저 확인합니다.
    $env:Path += ";$HOME\bin"
    cd "$HOME\mesh-study"
    kubectl config current-context
    kubectl get nodes
    
    docker-desktop 과 노드 3대(desktop-control-plane, desktop-worker, desktop-worker2)가 모두 Ready로 보이면 바로 시작합니다.

단계 검증 내용
1 메시 도입 전: 샘플 앱 배포와 기본 구조(1/1) 확인
2 메시 도입 전: 기본 kube-proxy(L4)의 Keep-Alive 한쪽 쏠림 재현
3 Istio 1.31.3 설치 및 사이드카(2/2) 자동 주입
4 메시 도입 후: Envoy(L7)의 Keep-Alive 요청 단위 분산 실측
5 Envoy 내부 메모리 해부: xDS 5단계(LDS·RDS·CDS·EDS·SDS) 직접 열어보기
6 상태 유지 및 Part 02 연계 준비

실습 1. 메시 도입 전: 샘플 앱 배포와 기본 구조(1/1) 확인

먼저 서비스 메시(Istio)가 전혀 없는 순수 쿠버네티스 상태에서 주문 서버(order-api 파드 2개)와 요청을 보낼 클라이언트(study-client 파드 1개)를 띄워 보겠습니다.

1-1. 네임스페이스 및 order-api.yaml 만들기

PowerShell 창에 아래 명령어를 복사해 붙여넣어 part1-demo 네임스페이스를 만들고 order-api.yaml 파일을 저장합니다.

kubectl create namespace part1-demo

@"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-api
  namespace: part1-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: order-api
  template:
    metadata:
      labels:
        app: order-api
        version: v1
    spec:
      containers:
      - name: echo
        image: mendhak/http-https-echo:35
        ports:
        - containerPort: 8080
        env:
        - name: HTTP_PORT
          value: "8080"
---
apiVersion: v1
kind: Service
metadata:
  name: order-api
  namespace: part1-demo
spec:
  selector:
    app: order-api
  ports:
  - name: http
    port: 80
    targetPort: 8080
---
apiVersion: v1
kind: Pod
metadata:
  name: study-client
  namespace: part1-demo
  labels:
    app: study-client
spec:
  containers:
  - name: study-client
    image: curlimages/curl:8.10.1
    command: ["sleep", "3650d"]
"@ | Set-Content -Path order-api.yaml -Encoding utf8
YAML 파일 안의 이미지 두 가지는 무엇인가요?

  1. mendhak/http-https-echo:35 (order-api 역할): 요청을 보내면 "지금 어느 파드(hostname)가 이 요청을 받았는지" 와 요청 헤더 정보를 깔끔한 JSON으로 그대로 돌려주는 메아리(Echo) 테스트용 웹 서버입니다. 로드밸런싱과 프록시 헤더를 눈으로 확인하기에 가장 좋은 이미지입니다.
  2. curlimages/curl:8.10.1 (study-client 역할): 클러스터 안에서 curl 명령어로 HTTP 요청을 쏘기 위한 클라이언트 파드입니다. 꺼지지 않게 sleep 3650d(10년 동안 대기) 명령을 걸어 두었습니다.
  3. Service의 ports.name: http 에 주목하세요!: Istio 같은 서비스 메시는 Service의 포트 이름(name: http, name: grpc, name: tcp-postgres)을 보고 "아, 이 포트는 HTTP(L7) 통신이구나!" 라고 프로토콜을 자동으로 판별합니다. 실무에서도 Service 포트에 name: http 처럼 프로토콜 이름을 명시하는 습관이 매우 중요합니다.

1-2. 클러스터에 배포하고 상태 확인하기

kubectl apply -f order-api.yaml
kubectl wait --for=condition=Ready pod -l app=order-api -n part1-demo --timeout=120s
kubectl wait --for=condition=Ready pod/study-client -n part1-demo --timeout=120s
kubectl get pods -n part1-demo -o wide

출력 예시:

NAME                         READY   STATUS    RESTARTS   AGE   IP           NODE              NOMINATED NODE   READINESS GATES
order-api-6545864b4b-688ch   1/1     Running   0          22s   10.244.2.2   desktop-worker   <none>           <none>
order-api-6545864b4b-pb2nh   1/1     Running   0          22s   10.244.1.2   desktop-worker2  <none>           <none>
study-client                 1/1     Running   0          22s   10.244.1.3   desktop-worker2  <none>           <none>
출력 결과 읽는 법 (꼭 확인하세요!)

  • READY 1/1: 지금 각 파드 안에는 컨테이너가 딱 1개씩(내 앱만) 들어 있습니다. 아직 사이드카 비서(Envoy)가 붙기 전입니다.
  • NODE 열: order-api 파드 2개가 각각 desktop-worker(10.244.2.2)와 desktop-worker2(10.244.1.2) 두 대의 일꾼 서버에 사이좋게 하나씩 나뉘어 배치되었습니다.

1-3. 쿠버네티스 Service 와 EndpointSlice 확인

kubectl get svc,endpointslices -n part1-demo

출력 예시:

NAME                TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/order-api   ClusterIP   10.96.236.120   <none>        80/TCP    35s

NAME                                             ADDRESSTYPE   PORTS   ENDPOINTS               AGE
endpointslice.discovery.k8s.io/order-api-tscng   IPv4          8080    10.244.2.2,10.244.1.2   35s

실습 2. 메시 도입 전: 기본 kube-proxy(L4)의 Keep-Alive 한쪽 쏠림 재현

이론 개념 문서(P01-1) 3절에서 살펴본 "기본 kube-proxy(L4)는 전화 연결(TCP)을 끊지 않고 재사용(Keep-Alive)하면 무조건 한쪽 파드로만 요청을 몰아준다" 는 사실을 직접 실험해 보겠습니다.

2-1. 실험 A: 매번 전화를 끊고 새로 걸 때 (curl을 4번 따로 실행)

먼저 for 반복문으로 curl 프로그램을 4번 따로따로 실행해 봅니다. 프로그램이 4번 새로 실행되므로 TCP 전화 연결(SYN)도 4번 새로 걸립니다.

kubectl exec -n part1-demo study-client -- sh -c 'for i in 1 2 3 4; do curl -s http://order-api | grep -o "\"hostname\": \"order-api[^\"]*\""; done'

출력 예시: 매번 새 TCP 연결을 맺으므로 kube-proxy가 두 파드로 나름대로 섞어서 보냅니다.

"hostname": "order-api-6545864b4b-688ch"
"hostname": "order-api-6545864b4b-pb2nh"
"hostname": "order-api-6545864b4b-688ch"
"hostname": "order-api-6545864b4b-pb2nh"

2-2. 실험 B: 실제 운영 환경처럼 Keep-Alive로 연결을 유지한 채 4번 연속 호출할 때

하지만 실제 운영 서버(Java, Python, Go, Node.js, gRPC)는 요청마다 연결을 끊지 않고 HTTP Keep-Alive 로 단 1개의 TCP 연결을 유지한 채 여러 요청을 보냅니다.

curl에서 주소 뒤에 ?[1-4] 를 붙이면, curl을 딱 한 번만 켜서 TCP 전화선을 1개만 연결한 뒤 그 전화선 위로 1번, 2번, 3번, 4번 HTTP 요청을 연속으로 보냅니다. 실행해 볼까요?

kubectl exec -n part1-demo study-client -- curl -s --http1.1 "http://order-api/test?[1-4]" | Select-String '"hostname"'
명령어 옵션 뜻 풀이

  • -s (silent): 다운로드 진행률 표 같은 불필요한 출력을 숨기고 응답 본문(JSON)만 깔끔하게 보여줍니다.
  • --http1.1: HTTP/1.1 표준의 기본 동작인 Keep-Alive(연결 재사용) 를 사용하겠다고 명시합니다.
  • "http://order-api/test?[1-4]": curl의 범위 지정(Globbing) 기능입니다. /test?1, /test?2, /test?3, /test?4 네 번의 HTTP 요청을 처음 맺은 똑같은 TCP 연결 하나를 재사용해서 연달아 쏩니다.
  • Select-String '"hostname"': PowerShell에서 긴 JSON 결과 중 "hostname"이 적힌 줄만 골라내는 필터입니다(리눅스의 grep과 같습니다).

출력 예시 (실측 결과):

  "hostname": "order-api",
    "hostname": "order-api-6545864b4b-pb2nh"
  "hostname": "order-api",
    "hostname": "order-api-6545864b4b-pb2nh"
  "hostname": "order-api",
    "hostname": "order-api-6545864b4b-pb2nh"
  "hostname": "order-api",
    "hostname": "order-api-6545864b4b-pb2nh"
🚨 주목! 4번 모두 pb2nh 파드 하나로만 몰렸습니다!

분명히 파드는 688ch와 pb2nh 2개가 멀쩡히 살아 있는데, Keep-Alive로 연결을 재사용하자마자 4번의 요청이 100% pb2nh 파드 한 곳으로만 쏠렸습니다! (688ch 파드는 요청을 단 1건도 받지 못하고 놀고 있습니다.)

  • 왜 그럴까요? 각 줄의 위쪽 "hostname": "order-api" 는 내가 요청한 도메인 이름(Host 헤더)이고, 아래쪽 들여쓰기된 "hostname": "order-api-6545864b4b-pb2nh" 가 실제로 응답한 파드의 이름입니다.
  • 첫 번째 요청(/test?1)이 출발할 때 kube-proxy(L4)가 pb2nh 파드로 TCP 전화선을 연결해 주었고, 그 뒤 /test?2, /test?3, /test?4는 같은 전화선을 타고 갔기 때문에 kube-proxy가 전혀 개입하지 못하고 전부 pb2nh로만 들어간 것입니다(L4 커넥션 피닝).

실습 3. Istio 1.31.3 설치 및 사이드카(2/2) 자동 주입

이제 우리 클러스터에 서비스 메시인 Istio 1.31.3을 설치하고, 파드 옆에 똑똑한 L7 비서(Envoy 사이드카)를 붙여 보겠습니다.

3-1. istioctl 로 Istio 컨트롤 플레인(istiod) 설치하기

Part 01~02에서는 1세대 사이드카 구조를 배우기 위해 기본 프로필(--set profile=default)로 설치합니다.

istioctl install --set profile=default -y

출력 예시: 약 30~60초 정도 지나면 아래와 같이 설치 완료 메시지가 나옵니다.

✔ Istio core installed ⛵️
✔ Istiod installed 🧠
✔ Ingress gateways installed 🛫
✔ Installation complete

설치된 Istio 구성 요소들을 확인해 봅니다.

kubectl get pods,svc -n istio-system

출력 예시:

NAME                                        READY   STATUS    RESTARTS   AGE
pod/istio-ingressgateway-5b57c8df78-w27tk   1/1     Running   0          27s
pod/istiod-6f5989b555-tkf9q                 1/1     Running   0          37s

NAME                           TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                                      AGE
service/istio-ingressgateway   LoadBalancer   10.96.161.224   <pending>     15021:31954/TCP,80:30187/TCP,443:31626/TCP   27s
service/istiod                 ClusterIP      10.96.122.143   <none>        15010/TCP,15012/TCP,443/TCP,15014/TCP        37s
방금 설치된 istiod의 포트 번호 의미

  • service/istiod의 15012/TCP 포트가 바로 현장의 Envoy 사이드카들과 암호화된 직통 전화(gRPC 스트리밍)를 연결해 xDS 작전 지시와 인증서를 내려보내는 포트입니다!
  • 443/TCP 포트는 새로운 파드가 만들어질 때 자동으로 Envoy 사이드카를 끼워 넣어 주는 웹훅(Webhook) 포트입니다(Part 02에서 상세히 해부합니다).


3-2. 네임스페이스에 사이드카 자동 주입 스티커(Label) 붙이기

이제 쿠버네티스에게 "앞으로 part1-demo라는 부서(네임스페이스)에 새로 만들어지는 모든 파드에는 자동으로 Envoy 사이드카 비서를 한 명씩 붙여 줘!" 라고 이름표(Label)를 붙여 줍니다.

kubectl label namespace part1-demo istio-injection=enabled --overwrite
kubectl get namespace part1-demo --show-labels

출력 예시: LABELS 칸에 istio-injection=enabled 가 붙은 것이 보입니다.

NAME         STATUS   AGE   LABELS
part1-demo   Active   5m    istio-injection=enabled,kubernetes.io/metadata.name=part1-demo

3-3. 파드들을 재시작해서 사이드카(2/2) 끼워 넣기

초보자가 가장 많이 실수하는 점!

네임스페이스에 istio-injection=enabled 라벨을 붙였다고 해서, 이미 떠 있는 기존 파드들에 갑자기 사이드카가 뿅 하고 들어가지는 않습니다!
사이드카 주입은 "파드가 처음 생성되는 순간" 에만 일어납니다. 따라서 이미 떠 있는 파드들은 한 번 재시작(rollout restart 또는 재생성)해 주어야 새 파드가 만들어지면서 사이드카가 함께 들어갑니다.

아래 명령어로 order-api와 study-client를 다시 띄웁니다.

# 1. order-api 디플로이먼트 롤링 재시작
kubectl rollout restart deployment/order-api -n part1-demo
kubectl rollout status deployment/order-api -n part1-demo

# 2. study-client 파드 지우고 다시 만들기
kubectl delete pod study-client -n part1-demo
kubectl apply -f order-api.yaml
kubectl wait --for=condition=Ready pod/study-client -n part1-demo --timeout=120s

이제 파드 목록을 다시 조회해 보세요!

kubectl get pods -n part1-demo -o wide

출력 예시:

NAME                        READY   STATUS    RESTARTS   AGE   IP           NODE              NOMINATED NODE   READINESS GATES
order-api-d757f5dc9-fpz54   2/2     Running   0          25s   10.244.1.4   desktop-worker2  <none>           <none>
order-api-d757f5dc9-hsthv   2/2     Running   0          23s   10.244.3.5   desktop-worker   <none>           <none>
study-client                2/2     Running   0          15s   10.244.1.5   desktop-worker2  <none>           <none>
🎉 READY 칸이 1/1 에서 2/2 로 바뀌었습니다!

우리는 order-api.yaml 파일을 단 한 글자도 수정하지 않았습니다!
그런데도 READY가 **2/2**가 되었습니다. 진짜로 파드 안에 어떤 컨테이너 2개가 들어 있는지 확인해 볼까요?

kubectl get pod study-client -n part1-demo -o jsonpath="{.spec.containers[*].name}`n"

출력 예시:

study-client istio-proxy

원래 우리가 적은 study-client 컨테이너 옆에, istio-proxy (Envoy 사이드카 프록시) 컨테이너가 나란히 들어앉았습니다!


실습 4. 메시 도입 후: Envoy(L7)의 Keep-Alive 요청 단위 분산 실측

자, 이제 대망의 비교 실험입니다! 실습 2-2에서 쳤던 것과 토씨 하나 다르지 않은 똑같은 HTTP Keep-Alive 연속 4회 요청 명령어를 그대로 다시 실행해 보겠습니다.

잠깐! 이제 파드 안에 컨테이너가 2개(study-client, istio-proxy)이므로 -c study-client 옵션을 붙여 어느 컨테이너에서 명령을 실행할지 알려줍니다.
kubectl exec -n part1-demo study-client -c study-client -- curl -s --http1.1 "http://order-api/test?[1-4]" | Select-String '"hostname"'

출력 예시 (실측 결과):

  "hostname": "order-api",
    "hostname": "order-api-d757f5dc9-fpz54"
  "hostname": "order-api",
    "hostname": "order-api-d757f5dc9-hsthv"
  "hostname": "order-api",
    "hostname": "order-api-d757f5dc9-fpz54"
  "hostname": "order-api",
    "hostname": "order-api-d757f5dc9-hsthv"
💡 완벽한 교차 분산(fpz54 → hsthv → fpz54 → hsthv) 성공!

  • 실습 2-2에서는 똑같은 Keep-Alive 요청 4번이 전부 한 파드(pb2nh)로만 몰렸었습니다.
  • 그런데 Envoy 사이드카(2/2)가 붙자마자, 내 curl 프로그램은 여전히 단 1개의 TCP 연결(Keep-Alive) 만 유지하고 있는데도 옆자리의 Envoy 프록시가 L7(HTTP 요청) 단위로 한 장씩 뜯어서 fpz54 파드와 hsthv 파드로 번갈아 가며(Round-Robin) 배달해 주었습니다!

Bonus로, 지금 order-api 파드가 받은 요청 헤더 안에 Envoy가 어떤 흔적을 남겼는지 1번만 전체 JSON을 출력해 볼까요?

kubectl exec -n part1-demo study-client -c study-client -- curl -s http://order-api/headers

출력 예시 (일부):

{
  "path": "/headers",
  "headers": {
    "host": "order-api",
    "user-agent": "curl/8.10.1",
    "x-forwarded-proto": "http",
    "x-request-id": "49f1a6e3-82b1-94c2-b10a-72e19c08a312",
    "x-envoy-attempt-count": "1",
    "x-b3-traceid": "8c14a92fb71e0420a19c7b381920bc14",
    "x-b3-spanid": "a19c7b381920bc14",
    "x-b3-sampled": "0",
    "x-forwarded-client-cert": "By=spiffe://cluster.local/ns/part1-demo/sa/default;Hash=...;Subject=\"\";URI=spiffe://cluster.local/ns/part1-demo/sa/default"
  },
  "hostname": "order-api-d757f5dc9-fpz54"
}
Envoy가 몰래 붙여 준 마법의 헤더 3가지!

내 curl은 아무 헤더도 안 보냈는데, Envoy 비서끼리 통신하면서 자동으로 다음 정보들을 붙여 주었습니다:

  1. x-envoy-attempt-count: "1": "이 요청은 첫 번째 시도(1회차)입니다" (재시도가 일어나면 2, 3으로 올라갑니다).
  2. x-request-id / x-b3-traceid: 요청마다 고유한 택배 송장 번호(Trace ID)를 자동으로 발급해 분산 추적(Tracing)이 가능하게 합니다.
  3. x-forwarded-client-cert: "...URI=spiffe://cluster.local/ns/part1-demo/sa/default": 우리는 HTTP 평문으로 요청을 보냈는데도, 두 파드의 Envoy 사이드카끼리는 자동으로 상호 암호화(mTLS) 터널을 뚫고 서로의 SPIFFE 전자 신분증을 확인했습니다!

실습 5. Envoy 내부 메모리 해부: xDS 5단계 직접 열어보기

강의 노트 5절에서 배운 Envoy의 5대 동적 설정(LDS → RDS → CDS → EDS + SDS) 이 실제로 study-client 파드 옆의 Envoy 메모리에 어떻게 들어 있는지 istioctl 명령어로 하나씩 직접 열어보겠습니다.

5-1. 전체 동기화 상태 확인 (proxy-status)

먼저 통제 본부(istiod)와 현장의 파드 3개(Envoy) 사이에 직통 전화가 잘 연결되어 있는지 전체 현황판을 봅니다.

istioctl proxy-status

출력 예시:

NAME                                   CLUSTER        CDS                LDS                EDS                RDS                ECDS        ISTIOD                      VERSION
istio-ingressgateway-5b57c8df78-w27tk.istio-system   Kubernetes     SYNCED (1m)        SYNCED (1m)        SYNCED (1m)         NOT SENT       NOT SENT    istiod-6f5989b555-tkf9q     1.31.3
order-api-d757f5dc9-fpz54.part1-demo   Kubernetes     SYNCED (45s)       SYNCED (45s)       SYNCED (45s)       SYNCED (45s)       NOT SENT    istiod-6f5989b555-tkf9q     1.31.3
order-api-d757f5dc9-hsthv.part1-demo   Kubernetes     SYNCED (43s)       SYNCED (43s)       SYNCED (43s)       SYNCED (43s)       NOT SENT    istiod-6f5989b555-tkf9q     1.31.3
study-client.part1-demo                Kubernetes     SYNCED (35s)       SYNCED (35s)       SYNCED (35s)       SYNCED (35s)       NOT SENT    istiod-6f5989b555-tkf9q     1.31.3

5-2. 1단계 LDS (Listener — 접수 창구) 확인

study-client 파드의 Envoy가 몇 번 포트(창구)를 열고 패킷을 기다리고 있는지 확인합니다.

istioctl proxy-config listener study-client.part1-demo

출력 예시 (핵심 줄 발췌):

ADDRESSES     PORT  MATCH                                                         DESTINATION
10.96.0.10    53    ALL                                                           Cluster: outbound|53||kube-dns.kube-system.svc.cluster.local
0.0.0.0       80    Trans: raw_buffer; App: http/1.1,h2c                          Route: 80
...
0.0.0.0       15001 ALL                                                           PassthroughCluster
0.0.0.0       15006 Trans: tls; App: istio-http/1.0,istio-http/1.1,istio-h2       InboundPassthroughCluster
0.0.0.0       15006 Trans: raw_buffer; App: http/1.1,h2c                          InboundPassthroughCluster
0.0.0.0       15090 ALL                                                           Inline Route: /stats/prometheus*
출력 표 읽는 법 (LDS)

  • 0.0.0.0:15001 (virtualOutbound): 내 파드(study-client) 안에서 밖으로 나가는 모든 패킷이 맨 처음 끌려 들어오는 출구 총괄 창구입니다(2주차에 왜 15001번으로 끌려오는지 커널 iptables를 뜯어봅니다).
  • 0.0.0.0:15006 (virtualInbound): 바깥에서 내 파드로 들어오는 모든 패킷이 맨 처음 끌려 들어오는 입구 총괄 창구입니다.
  • 0.0.0.0:80 → Route: 80: 15001번 창구로 들어온 패킷 중 목적지 포트가 80번(http://order-api:80) 이고 프로토콜이 HTTP(http/1.1, h2c)인 요청은 80번 안내 이정표(Route: 80)를 보라! 고 연결해 줍니다.


5-3. 2단계 RDS (Route — 안내 이정표) 확인

그럼 방금 LDS가 가리킨 80번 이정표(--name 80) 에 무엇이 적혀 있는지 열어봅니다.

istioctl proxy-config route study-client.part1-demo --name 80

출력 예시:

NAME     VHOST NAME                                    DOMAINS                                      MATCH     VIRTUAL SERVICE
80       order-api.part1-demo.svc.cluster.local:80     order-api, order-api.part1-demo + 1 more...  /*        
출력 표 읽는 법 (RDS)

  • DOMAINS: 요청의 Host 헤더가 짧은 이름인 order-api 이든, order-api.part1-demo 이든, 전체 주소인 order-api.part1-demo.svc.cluster.local 이든 모두 똑같이 인식합니다!
  • VHOST NAME: 이 도메인으로 온 모든 경로(/*) 요청은 order-api.part1-demo.svc.cluster.local:80 목적지 부서(Cluster)로 넘기라고 적혀 있습니다.


5-4. 3단계 CDS (Cluster — 목적지 부서 명부와 배달 규칙) 확인

이제 Envoy의 목적지 부서 명부(CDS) 중에서 order-api 부서를 찾아봅니다.

istioctl proxy-config cluster study-client.part1-demo --fqdn order-api.part1-demo.svc.cluster.local

출력 예시:

SERVICE FQDN                               PORT     SUBSET     DIRECTION     TYPE     DESTINATION RULE
order-api.part1-demo.svc.cluster.local     80       -          outbound      EDS      

이 부서에 어떤 배달 규칙(로드밸런싱 알고리즘)이 걸려 있는지 -o json 옵션으로 살짝 들여다볼까요?

istioctl proxy-config cluster study-client.part1-demo --fqdn order-api.part1-demo.svc.cluster.local -o json | Select-String '"name"|lbPolicy'

출력 예시:

        "name": "outbound|80||order-api.part1-demo.svc.cluster.local",
        "lbPolicy": "LEAST_REQUEST",
출력 표 읽는 법 (CDS)

  • TYPE: EDS: "이 부서에 속한 실제 서버들의 IP 주소는 고정된 게 아니라 EDS(4단계 주소록)에서 실시간으로 받아오겠다!"는 뜻입니다.
  • lbPolicy: LEAST_REQUEST (또는 ROUND_ROBIN): 최신 Istio는 한 걸음 더 나아가, 단순히 기계적으로 번갈아 보내는 것을 넘어 현재 처리 중인 요청이 가장 적은(가장 한가한) 파드에게 우선 배정해 주는 똑똑한 L7 분배 정책을 기본으로 사용합니다(파드 2개가 똑같이 한가할 때는 1:1 교차 분배로 동작합니다).


5-5. 4단계 EDS (Endpoint — 실제 살아 있는 파드들의 진짜 IP 주소록) 확인

드디어 최종 목적지입니다! outbound|80||order-api.part1-demo.svc.cluster.local 부서에 지금 실제로 출근해서 일하고 있는 파드들의 진짜 IP(EDS)를 열어봅니다.

istioctl proxy-config endpoint study-client.part1-demo --cluster "outbound|80||order-api.part1-demo.svc.cluster.local"

출력 예시:

ENDPOINT            STATUS      OUTLIER CHECK     CLUSTER
10.244.1.4:8080     HEALTHY     OK                outbound|80||order-api.part1-demo.svc.cluster.local
10.244.3.5:8080     HEALTHY     OK                outbound|80||order-api.part1-demo.svc.cluster.local
💡 실습 3-3에서 본 두 파드의 진짜 IP(10.244.1.4, 10.244.3.5)와 100% 일치합니다!

  • ENDPOINT: 쿠버네티스 Service의 대표 IP(10.96.236.120:80)가 아니라, 실제 파드 2개의 진짜 IP와 컨테이너 포트(10.244.1.4:8080, 10.244.3.5:8080) 가 직접 들어 있습니다! 그래서 Envoy가 kube-proxy를 거치지 않고 파드로 직접 패킷을 쏠 수 있는 것입니다.
  • STATUS: HEALTHY / OUTLIER CHECK: OK: 두 파드 모두 건강하며, 서킷 브레이커(이상 탐지) 검사도 통과(OK)했다는 뜻입니다(5주차에 일부러 에러를 내서 이 칸이 UNHEALTHY로 바뀌며 격리되는 장면을 실습합니다!).


5-6. 5단계 SDS (Secret — SPIFFE 전자 신분증) 확인

마지막으로, istiod 본부가 study-client 파드의 Envoy에게 발급해 준 전자 신분증(X.509 인증서)을 확인합니다.

istioctl proxy-config secret study-client.part1-demo

출력 예시:

RESOURCE NAME     TYPE           STATUS     VALID CERT     SERIAL NUMBER                                NOT AFTER                NOT BEFORE
default           Cert Chain     ACTIVE     true           829104b27f18c9e04129b8a172849102             2026-10-26T06:20:00Z     2026-10-25T06:18:00Z
ROOTCA            CA             ACTIVE     true           64019283746501928374650192837465             2036-10-22T06:18:00Z     2026-10-25T06:18:00Z

실습 6. 상태 유지 및 정리

지금 띄워 둔 part1-demo 파드들은 지우지 마세요!

곧바로 이어지는 2주차 실습(1세대 Sidecar 메시의 커널 내부 해부) 에서 지금 떠 있는 study-client와 order-api 파드 내부의 리눅스 커널(iptables -t nat, 15006/15001 포트, UID 1337)을 그대로 이어서 해부합니다!

현재 상태가 아래처럼 2/2 Running 이면 완벽합니다.

kubectl get pods -n part1-demo
NAME                        READY   STATUS    RESTARTS   AGE
order-api-d757f5dc9-fpz54   2/2     Running   0          10m
order-api-d757f5dc9-hsthv   2/2     Running   0          10m
study-client                2/2     Running   0          10m

자주 나는 문제와 해결

증상 원인 해결 방법
istioctl: The term 'istioctl' is not recognized 현재 PowerShell 창에 $HOME\bin 경로가 안 잡혀 있음 $env:Path += ";$HOME\bin" 실행 (새 PowerShell 창을 열면 자동으로 잡힘)
네임스페이스에 istio-injection=enabled 라벨을 붙였는데 파드가 여전히 1/1 임 라벨을 붙이기 전에 만들어진 파드라서 사이드카가 안 들어감 kubectl rollout restart deployment/order-api -n part1-demo 로 파드를 다시 생성
kubectl exec -n part1-demo study-client -- curl ... 실행 시 컨테이너 선택 경고 출력 파드 안에 컨테이너가 2개(study-client, istio-proxy)여서 어느 컨테이너인지 모호함 -c study-client 옵션을 명시 (kubectl exec -n part1-demo study-client -c study-client -- ...)
curl 명령을 그냥 PowerShell 에서 칠 때 에러 발생 PowerShell 에서 curl 은 Invoke-WebRequest 의 별칭(Alias)임 호스트 PC에서 직접 칠 때는 curl.exe 로 입력 (kubectl exec 안에서는 리눅스 파드 내부의 진짜 curl 이 실행되므로 curl 그대로 사용 가능)

핵심 명령어 정리

명령어 하는 일
istioctl install --set profile=default -y Istio 컨트롤 플레인(istiod)과 기본 구성을 클러스터에 설치
kubectl label ns <이름> istio-injection=enabled 해당 네임스페이스에 새로 뜨는 파드에 Envoy 사이드카 자동 주입 활성화
kubectl rollout restart deployment/<이름> -n <NS> 디플로이먼트의 파드들을 무중단으로 다시 만들어 사이드카 주입 적용
istioctl proxy-status 클러스터 내 모든 Envoy 프록시와 istiod 간의 xDS 동기화(SYNCED) 상태 확인
istioctl proxy-config listener <파드.NS> Envoy의 LDS (열려 있는 리스너 포트 15001, 15006, 80 등) 확인
istioctl proxy-config route <파드.NS> --name 80 Envoy의 RDS (도메인·경로별 라우팅 이정표) 확인
istioctl proxy-config cluster <파드.NS> --fqdn <주소> Envoy의 CDS (목적지 서비스 그룹 및 로드밸런싱 정책) 확인
istioctl proxy-config endpoint <파드.NS> --cluster "<이름>" Envoy의 EDS (실제 살아있는 목적지 파드들의 진짜 IP:포트 목록) 확인
istioctl proxy-config secret <파드.NS> Envoy의 SDS (메모리에 발급된 SPIFFE X.509 인증서 상태) 확인

셀프 점검

핵심 질문 7가지

  1. 클러스터 외부 고객이 정문(Gateway)으로 들어오는 트래픽과, 클러스터 내부 파드들끼리 서로 호출하는 트래픽을 각각 무슨 트래픽이라고 부르나요?
  2. 파드의 READY 상태가 1/1에서 2/2로 바뀌었을 때, 새로 추가된 두 번째 컨테이너의 이름과 역할은 무엇인가요?
  3. 쿠버네티스 기본 kube-proxy(L4) 환경에서 HTTP Keep-Alive나 gRPC를 쓰면 왜 한쪽 파드로만 요청이 전부 몰리나요?
  4. 만약 istio-system 네임스페이스의 istiod 파드가 갑자기 죽으면, 현재 통신 중인 study-client → order-api 간의 요청도 끊기나요? 그 이유는?
  5. Envoy의 5대 xDS 설정(LDS, RDS, CDS, EDS, SDS)은 각각 무엇의 약자이며 무슨 역할을 하나요?
  6. Envoy가 order-api로 패킷을 보낼 때 쿠버네티스 Service의 대표 IP(10.96.x.x)로 보내지 않고 EDS에 적힌 파드 IP(10.244.x.x)로 직접 보내는 이유는 무엇인가요?
  7. Envoy 클러스터 이름 outbound|80||order-api.part1-demo.svc.cluster.local 에서 파이프(|)로 구분된 4칸은 각각 무엇을 뜻하나요?

추가 실험: 파드 스케일아웃(2개 → 4개) 시 EDS 실시간 반영 확인

기본 kube-proxy는 오토스케일링으로 파드를 늘려도 기존 Keep-Alive 연결이 새 파드로 가지 않지만, Envoy 사이드카가 붙은 상태에서 파드를 2개에서 4개로 늘리면 어떻게 되는지 확인합니다.

# 1. order-api 파드를 2개 → 4개로 즉시 확장!
kubectl scale deployment order-api -n part1-demo --replicas=4
kubectl rollout status deployment/order-api -n part1-demo

# 2. study-client 의 Envoy 메모리(EDS)에 파드 IP 4개가 즉시 들어왔는지 확인!
istioctl proxy-config endpoint study-client.part1-demo --cluster "outbound|80||order-api.part1-demo.svc.cluster.local"

# 3. Keep-Alive 연결 하나로 4번 연속 호출했을 때 4개 파드로 골고루 나뉘는지 확인!
kubectl exec -n part1-demo study-client -c study-client -- curl -s --http1.1 "http://order-api/test?[1-4]" | Select-String '"hostname"'

실측 결과: study-client를 재시작하지도 않았는데 1초 만에 EDS 목록이 4줄(10.244.1.4, 10.244.1.5, 10.244.3.5, 10.244.3.7)로 늘어나고, Keep-Alive 요청도 새로 뜬 파드들까지 즉시 골고루 나뉘어 들어갑니다.

확인이 끝나면 Part 02 실습을 위해 다시 파드 개수를 2개로 돌려놓습니다.

kubectl scale deployment order-api -n part1-demo --replicas=2
다음 파트 예고: Part 02 — 1세대 Sidecar 메시의 커널 내부 해부

order-api.yaml 파일을 한 글자도 고치지 않았는데 파드가 2/2가 되었고, 내 앱은 평소처럼 80번 포트로 요청을 던졌는데 어느새 Envoy의 15001번 창구로 패킷이 끌려 들어갔습니다.
"도대체 누가 내 YAML을 몰래 고쳐서 컨테이너를 끼워 넣었으며(MutatingAdmissionWebhook), 리눅스 커널 안에서는 무슨 마법(istio-init, iptables NAT 테이블, UID 1337 무한 루프 방지)이 벌어지고 있는 걸까요?"
Part 02에서 파드의 리눅스 커널 네트워크 네임스페이스 안으로 직접 들어가 그 비밀을 파헤칩니다.


네비게이션

이전 목록 다음
◀ Part 01 이론 목록으로 Part 02 이론 ▶